/+
Library dbapi - Simple database api for the D programming language.

Copyright (C) 2007 Myron Alexander (myron.alexander...gmail.com)

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+/

module dbapi.internal.sqlite3.connection;

private import std.string : toStringz;

private import dbapi.base;

private import dbapi.internal.sqlite3.definitions;
private import dbapi.internal.sqlite3.internalconnection;
private import dbapi.internal.sqlite3.transaction;
private import dbapi.internal.sqlite3.cursor;
private import dbapi.internal.sqlite3.error;

private import sqlite.sqlite3;

private enum ConnectionStates {
     OPEN     = 1
   , CLOSED   = 2
   , CLOSING  = 3
}

private typedef void delegate (ConnectionStates state) ConnectionStateEvent;

/**
 * Each class that implements this interface defines a connection to the
 * database.
 */
interface Connection {

   //----------------------------------------------------
   // Methods that affect the connection state
   //----------------------------------------------------

   bool     close ();

   //----------------------------------------------------
   // Methods that query the connection state
   //----------------------------------------------------

   bool     isOpen ();
   bool     isClosing ();
   bool     isClosed ();

   //----------------------------------------------------
   // Methods that provide a query context
   //----------------------------------------------------

   Cursor   cursor ();

   //----------------------------------------------------
   // Methods that affect the transaction state
   //----------------------------------------------------

   bool     commit ();
   void     rollback ();

   //----------------------------------------------------
   // Methods that query the transaction state
   //----------------------------------------------------

   bool     inTransaction ();

   //----------------------------------------------------
   // Connection properties
   //----------------------------------------------------

   sqlite3* dbcon ();
}

private class ConnectionImpl : Connection {

   private this (   sqlite3*               dbcon
                  , char[]                 dsn
                  , TransactionModeHandler transModeHandler) {

      m_conn.dsn                 = dsn;
      m_conn.dbcon               = dbcon;
      m_conn.transModeHandler    = transModeHandler;
      m_conn.raiseConnStateEvent = &connectionStateEvent;
      m_state                    = m_operationalState;

      m_internalConnection       = new InternalConnectionImpl ();
   }

   bool close () {
      return m_state.close (&m_conn);
   }

   bool isOpen () {
      return m_state.isOpen ();
   }

   bool isClosing () {
      return m_state.isClosing ();
   }

   bool isClosed () {
      return m_state.isClosed ();
   }

   Cursor cursor () {
      return m_state.cursor (m_internalConnection);
   }

   bool commit () {
      return m_state.commit (&m_conn);
   }

   void rollback () {
      m_state.rollback (&m_conn);
   }

   bool inTransaction () {
      return m_state.inTransaction (&m_conn);
   }

   sqlite3* dbcon () {
      return m_state.dbcon (&m_conn);
   }


   private void connectionStateEvent (ConnectionStates state) {

      switch (state) {

         case ConnectionStates.CLOSED:

            if (m_state is m_closedState) {

               throw (new SqlInterfaceException (
                  "Invalid request for closed transition on closed state"))
                  .setLongMsg (
                     "Invalid request for closed transition on closed state. This "
                     "is an internal error where a request for a state change to "
                     "the closed state is made on a connection that is in the "
                     "closed state. Please log a bug report.")
                  .setProperty ("ConnectionState", state);
            }

            // Change the state to the closed state.
            m_state = m_closedState;

            break;

         case ConnectionStates.CLOSING:

            if (m_state is m_closingState) {

               throw (new SqlInterfaceException (
                  "Invalid transition to same connection state"))
                  .setLongMsg (
                     "Invalid transition to same connection state. This is an "
                     "internal error where a state change request was made to "
                     "the same state. Please log a bug report.")
                  .setProperty ("ConnectionState", state);

            } else if (m_state is m_closedState) {

               throw (new SqlInterfaceException (
                  "Invalid transition from closed connection state"))
                  .setLongMsg (
                     "Invalid transition from closed connection state. This is "
                     "an internal error where the a state change from the "
                     "closed state was requested. Please log a bug report.")
                  .setProperty ("ConnectionState", state);
            }

            // Change the state to the closing state.
            m_state = m_closingState;

            break;

         case ConnectionStates.OPEN:

            throw (new SqlInterfaceException (
               "Invalid connection state request"))
               .setLongMsg (
                  "Invalid connection state request. This is an internal error "
                  "where the requested state, OPEN, is not a valid transition. "
                  "The connection starts in the OPEN state and the state "
                  "design never calls for a transition to the OPEN state. "
                  "Please log a bug report.")
               .setProperty ("ConnectionState", state);

         default:

            throw (new SqlInterfaceException (
               "Invalid connection state value"))
               .setLongMsg (
                  "Invalid connection state value. This is an internal error "
                  "where the connection state value passed into the connection "
                  "event is not one of OPEN, CLOSED, CLOSING. Please log a "
                  "bug report.")
               .setProperty ("ConnectionState", state);
      }
   }

   /**
    * Read-only view of connection details for internal consumption.
    */
   private class InternalConnectionImpl : InternalConnection {

      string dsn () {
         return m_conn.dsn;
      }
      
      bool isOpen () {
         return m_state.isOpen ();
      }

      TransactionModeHandler transModeHandler () {
         return m_conn.transModeHandler;
      }

      sqlite3* dbcon () {
         return m_conn.dbcon;
      }
   }

   InternalConnection   m_internalConnection;
   ConnectionState      m_state;
   ConnectionContext    m_conn;
}

//BUG: If I make private, then linker does not find symbols associated with it.
interface ConnectionState {

   //----------------------------------------------------
   // Methods that affect the connection state
   //----------------------------------------------------

   bool     close (ConnectionContext *conn);

   //----------------------------------------------------
   // Methods that query the connection state
   //----------------------------------------------------

   bool     isOpen ();
   bool     isClosing ();
   bool     isClosed ();

   //----------------------------------------------------
   // Methods that provide a query context
   //----------------------------------------------------

   Cursor   cursor (InternalConnection iconn);

   //----------------------------------------------------
   // Methods that affect the transaction state
   //----------------------------------------------------

   bool     commit (ConnectionContext *conn);
   void     rollback (ConnectionContext *conn);

   //----------------------------------------------------
   // Methods that query the transaction state
   //----------------------------------------------------

   bool     inTransaction (ConnectionContext *conn);

   //----------------------------------------------------
   // Connection properties
   //----------------------------------------------------

   sqlite3* dbcon (ConnectionContext *conn);
}


/**
 * Closed connection state behaviour.
 */
private class ClosedState : ConnectionState {

   static this () {
      m_exception = new SqlClosedException (
         "The connection is closed. No further operations on the connection "
         "are permitted.");
   }

   static SqlClosedException   m_exception;

   bool close (ConnectionContext *conn) {
      throw m_exception;
   }

   bool commit (ConnectionContext *conn) {
      throw m_exception;
   }

   void rollback (ConnectionContext *conn) {
      throw m_exception;
   }

   Cursor cursor (InternalConnection iconn) {
      throw m_exception;
   }

   sqlite3* dbcon (ConnectionContext *conn) {
      throw m_exception;
   }

   bool inTransaction (ConnectionContext *conn) {
      throw m_exception;
   }

   bool isOpen () {
      return false;
   }

   bool isClosing () {
      return false;
   }

   bool isClosed () {
      return true;
   }
}

/**
 * Closing connection state behaviour.
 */
private class ClosingState : ConnectionState {

   static this () {
      m_exception = new SqlClosingException (
         "The connection is closing. No further operations on the connection "
         "are permitted.");
   }

   static SqlClosingException   m_exception;

   bool close (ConnectionContext *conn) {

      return doClose (conn);
   }

   bool commit (ConnectionContext *conn) {
      throw m_exception;
   }

   void rollback (ConnectionContext *conn) {
      throw m_exception;
   }

   Cursor cursor (InternalConnection iconn) {
      throw m_exception;
   }

   sqlite3* dbcon (ConnectionContext *conn) {
      throw m_exception;
   }

   bool inTransaction (ConnectionContext *conn) {
      throw m_exception;
   }

   bool isOpen () {
      return false;
   }

   bool isClosing () {
      return true;
   }

   bool isClosed () {
      return false;
   }
}

/**
 * Operational connection state behaviour.
 */
private class OperationalState : ConnectionState {

   /**
    * Close the connection.
    *
    * Returns:
    *   true when the connection has been closed or was closed.
    *   false when the connection cannot be closed due to open statements.
    */
   bool close (ConnectionContext *conn) {

      /* TODO - Check if it is possible to close the connection before
       *        attempting to close.
       */

      /* The closing procedure has been initiated. No further operations
       * on this connection will be permitted except for calls to this
       * close method.
       */
      conn.raiseConnStateEvent (ConnectionStates.CLOSING);

      if (conn.transModeHandler !is null) {
         /* The rollback is dependent on the transaction mode handler and is
          * not guaranteed to actually cause a rollback.
          */
         conn.transModeHandler.onClose ();
         delete conn.transModeHandler;
      }

      return doClose (conn);
   }

   bool commit (ConnectionContext *conn) {
      return conn.transModeHandler.commit ();
   }

   void rollback (ConnectionContext *conn) {
      conn.transModeHandler.rollback ();
   }

   Cursor cursor (InternalConnection iconn) {
      return new Cursor (iconn);
   }

   sqlite3* dbcon (ConnectionContext *conn) {
      return conn.dbcon;
   }

   bool inTransaction (ConnectionContext *conn) {
      return conn.transModeHandler.inTransaction ();
   }

   bool isOpen () {
      return true;
   }

   bool isClosing () {
      return false;
   }

   bool isClosed () {
      return false;
   }
}

private bool doClose (ConnectionContext *conn) {

   int rc = sqlite3_close (conn.dbcon);

   switch (rc) {
      case SQLITE_OK:
         conn.raiseConnStateEvent (ConnectionStates.CLOSED);
         break;

      case SQLITE_BUSY:
         return false;
         break;

      default:
         throw SqliteError!(SqlOperationalException) (
            conn.dbcon,
            "Error on closing database.")
         .setProperty ("DSN", conn.dsn);
   }

   return true;
}

private struct ConnectionContext {
   string                   dsn;
   sqlite3*                 dbcon;
   TransactionModeHandler   transModeHandler;
   ConnectionStateEvent     raiseConnStateEvent;
}

/**
 * Open a connection to the dsn provided.
 *
 * Returns:
 *    Connection object wrapping a valid connection to the sqlite database.
 */
public Connection createConnection (
              char[]             dsn
            , TransactionMode    transactionMode = TransactionMode.AUTO_COMMIT
            , TransactionType    transactionType = TransactionType.DEFAULT) {

   //
   // Create the connection.
   //

   sqlite3* dbcon;

   int rc = sqlite3_open (toStringz (dsn), &dbcon);

   if (SQLITE_OK != rc) {
      throw SqliteError!(SqlOperationalException) (
         dbcon,
         "Error on opening database")
      .setProperty ("DSN", dsn);
   }

   // Force closed the connection if a failure occurs after this point.
   // The result of the close operation is not tested as I have not thought
   // of a way to recover after this point. Under normal circumstances, the
   // close operation should not fail as the connection has just been opened
   // and no operations have been performed on it, thus no possibility of
   // BUSY.
   //
   // TODO: Investigate how this statement could fail and prepare a strategy.
   scope (failure) {
      sqlite3_close (dbcon);
   }

   //
   // Set the transaction mode handler.
   //
   // The transaction mode handler cannot be set before the connection is
   // opened as it creates statements and manages transaction context.
   //

   TransactionModeHandler transModeHandler =
      standardModeHandlerFactory.createTransactionModeHandler (
                                       dbcon, transactionMode, transactionType);

   Connection connection =
      new ConnectionImpl (dbcon, dsn, transModeHandler);

   return connection;
}

//------------------------------------------------------------------------------
// Module static variables and initialisation
//------------------------------------------------------------------------------

private static ConnectionState m_operationalState;
private static ConnectionState m_closingState;
private static ConnectionState m_closedState;

static this () {
   m_operationalState = new OperationalState ();
   m_closingState     = new ClosingState ();
   m_closedState      = new ClosedState ();
}

static ~this () {
   delete m_operationalState;
   delete m_closingState;
   delete m_closedState;
}
